refactor(RangePicker): centralize interaction flow#994
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
❌ Deploy failed
📋 Build log (last lines)🤖 Powered by surge-preview |
|||||||||
There was a problem hiding this comment.
Code Review
This pull request refactors focus and value change handling in the RangePicker component by introducing the useFocusControl and useRangeValueChange hooks. These hooks streamline the coordination of calendar value updates, partial/final submissions, and focus/blur states across multiple fields. The feedback suggests a critical fix in useRangeValueChange to allow smooth field switching when needConfirm is false and the previous field is empty, as well as adding optional chaining to selectorRef.current accesses in RangePicker.tsx to prevent potential runtime errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| // ============================= Utils ============================= | ||
| /** Check whether the target is the container itself or inside it. / 判断目标是否为容器自身或其子元素。 */ | ||
| function containsElement(container: Element | null, target: EventTarget | null) { |
There was a problem hiding this comment.
这个函数直接在 isTargetInContainers 里内联掉,不要额外写个函数
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #994 +/- ##
==========================================
- Coverage 98.81% 98.44% -0.37%
==========================================
Files 66 68 +2
Lines 2698 2830 +132
Branches 724 803 +79
==========================================
+ Hits 2666 2786 +120
- Misses 29 41 +12
Partials 3 3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| case 'switchPrevious': { | ||
| if (!needConfirm) { | ||
| const currentValue = getCalendarValue()[actionIndex]; | ||
| const currentEmpty = currentValue === null || currentValue === undefined; | ||
|
|
||
| if (!currentEmpty || allowEmpty[actionIndex]) { | ||
| // Going back may part-submit the current field, but must never | ||
| // finish the whole round before the previous field is edited. | ||
| // 返回上一个 field 时可以局部提交当前 field,但不能在用户修改 | ||
| // 上一个 field 前结束整轮提交。 | ||
| flushSubmit(actionIndex, false); | ||
| } else { | ||
| resetValue(actionIndex); | ||
| } | ||
| } | ||
|
|
||
| const previousPosition = triggeredFieldsRef.current.findIndex( | ||
| (field) => field.index === index, | ||
| ); | ||
| triggeredFieldsRef.current = triggeredFieldsRef.current.slice(0, previousPosition + 1); | ||
| recordTriggeredField(index, false); | ||
| setCurrentIndex(index); | ||
| break; | ||
| } |
There was a problem hiding this comment.
needConfirm 场景切回 previous field 时,这里不会 reset 当前未确认值,但后面会移除对应的 tracking。这样 CalendarValue 仍然保留,状态机却不再记录,后续 blur 是否可能直接走 finish,导致未确认值残留?

Summary
PickerTrigger.onCloseresponsible only for popup visibilityMotivation
RangePicker currently spreads part-submit and reset behavior across event handlers and an effect. This refactor prepares a single event-driven decision point for confirm and non-confirm flows, following the behavior discussed around #966.
Impact
This changes RangePicker's internal interaction handling only. The existing
useRangeValuehook remains responsible for value validation and final submission.Validation
ut lint:tscut lint— 0 errorsInteraction flow
canSwitch = 当前值非空 || allowEmpty[currentIndex]allFieldsTriggered = triggeredFields.length >= fieldCountmodifyswitchNextswitchPreviousfinishabortresetCurrentresetCurrentAndSwitchNextresetAllflowchart TB START["① 入口与 field-switch<br/>triggerChange(index, source, value?)"] --> ESC{"🔎 source === esc?"} ESC -- "是" --> ESC_RESET["🗑️ action = resetAll"] ESC -- "否" --> IDLE{"🔎 currentIndex === null?"} IDLE -- "是,source = blur" --> IDLE_RESET["🗑️ action = resetAll"] IDLE -- "是,其他事件" --> INIT["📌 初始化 currentIndex = index<br/>记录 triggered field"] IDLE -- "否" --> ROUTE{"🔎 source === field-switch?"} INIT --> ROUTE ROUTE -- "否" --> CURRENT_ENTRY["进入 ② 当前 field 事件"] ROUTE -- "是" --> FS_SAME{"🔎 index === currentIndex?"} FS_SAME -- "是" --> FS_ABORT_SAME["⛔ action = abort"] FS_SAME -- "否" --> FS_PREVIOUS{"🔎 是否为已访问的上一个 field?"} FS_PREVIOUS -- "是" --> FS_PREVIOUS_LOCK{"🔎 needConfirm<br/>且当前值非空、未确认<br/>且不允许空?"} FS_PREVIOUS_LOCK -- "是" --> FS_ABORT_PREVIOUS["⛔ action = abort"] FS_PREVIOUS_LOCK -- "否" --> FS_SWITCH_PREVIOUS["⬅️ action = switchPrevious"] FS_PREVIOUS -- "否" --> FS_NEXT{"🔎 index === nextIndex?"} FS_NEXT -- "否" --> FS_ABORT_INVALID["⛔ action = abort"] FS_NEXT -- "是" --> FS_CONFIRM{"🔎 needConfirm?"} FS_CONFIRM -- "否" --> FS_CAN_SWITCH{"🔎 canSwitch?"} FS_CAN_SWITCH -- "是" --> FS_SWITCH_NEXT["➡️ action = switchNext"] FS_CAN_SWITCH -- "否" --> FS_RESET_CURRENT["↩️ action = resetCurrent"] FS_CONFIRM -- "是" --> FS_CONFIRMED{"🔎 当前 field 已确认?"} FS_CONFIRMED -- "是" --> FS_SWITCH_CONFIRMED["➡️ action = switchNext"] FS_CONFIRMED -- "否" --> FS_ALLOW_EMPTY{"🔎 allowEmpty[currentIndex]?"} FS_ALLOW_EMPTY -- "是" --> FS_RESET_SWITCH["🔄 action = resetCurrentAndSwitchNext"] FS_ALLOW_EMPTY -- "否" --> FS_ABORT_LOCKED["⛔ action = abort"]flowchart TB START["② 当前 field 事件<br/>非 field-switch"] --> CURRENT_INDEX{"🔎 index === currentIndex?"} CURRENT_INDEX -- "否" --> CURRENT_ABORT["⛔ action = abort"] CURRENT_INDEX -- "是" --> SOURCE{"🔎 source 类型"} SOURCE -- "input / panel-intermediate" --> MODIFY["✏️ action = modify"] SOURCE -- "panel-final" --> PANEL_CONFIRM{"🔎 needConfirm?"} PANEL_CONFIRM -- "是" --> PANEL_MODIFY["✏️ action = modify"] PANEL_CONFIRM -- "否" --> PANEL_SWITCH["➡️ action = switchNext"] SOURCE -- "keyboard-submit / confirm" --> SUBMIT_VALID{"🔎 canSwitch?"} SUBMIT_VALID -- "是" --> SUBMIT_SWITCH["➡️ action = switchNext"] SUBMIT_VALID -- "否" --> SUBMIT_ABORT["⛔ action = abort"] SOURCE -- "blur" --> BLUR_ENTRY["进入 ③ blur 分支"]flowchart TB START["③ blur 分支"] --> BLUR_MODIFIED{"🔎 本轮是否修改过任意 field?"} BLUR_MODIFIED -- "否" --> BLUR_FINISH["✅ action = finish"] BLUR_MODIFIED -- "是" --> BLUR_CONFIRM{"🔎 needConfirm?"} BLUR_CONFIRM -- "否" --> BLUR_CAN_SWITCH{"🔎 canSwitch?"} BLUR_CAN_SWITCH -- "是" --> BLUR_SWITCH["➡️ action = switchNext"] BLUR_CAN_SWITCH -- "否" --> BLUR_RESET_INVALID["🗑️ action = resetAll"] BLUR_CONFIRM -- "是" --> BLUR_READY{"🔎 allFieldsTriggered<br/>且 canSwitch?"} BLUR_READY -- "否" --> BLUR_RESET_ALL["🗑️ action = resetAll"] BLUR_READY -- "是" --> BLUR_CURRENT_MODIFIED{"🔎 当前 field 是否修改过?"} BLUR_CURRENT_MODIFIED -- "否" --> BLUR_FINAL_SWITCH["➡️ action = switchNext"] BLUR_CURRENT_MODIFIED -- "是" --> BLUR_ALLOW_EMPTY{"🔎 allowEmpty[currentIndex]?"} BLUR_ALLOW_EMPTY -- "是" --> BLUR_RESET_SWITCH["🔄 action = resetCurrentAndSwitchNext"] BLUR_ALLOW_EMPTY -- "否" --> BLUR_RESET_UNCONFIRMED["🗑️ action = resetAll"]